﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

namespace Manoso.Data.FluentMap.Converter
{
    internal class MapVisitor : ExpressionVisitor
    {
        public StringBuilder Literal { get; private set; }

        private ParameterExpression _parameter;

        private readonly IMapAdaptor _adaptor;

        #region Constructor

        public MapVisitor(IMapAdaptor adaptor)
        {
            _adaptor = adaptor;
        }

        #endregion

        #region Overrides

        public override Expression Visit(Expression node)
        {
            if (node != null && node.NodeType == ExpressionType.Lambda && _parameter == null && Literal == null)
            {
                var lambda = (LambdaExpression) node;
                _parameter = lambda.Parameters[0];
                Literal = new StringBuilder(lambda.Body.ToString());
            }
            return base.Visit(node);
        }

        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            var methodName = node.Method.Name;
            if (methodName == "Any")
                Any(node);
            else if (methodName == "Contains")
                Contains(node);
            else if (methodName == "Equals")
                Equals(node);
            else if (methodName == "StartsWith")
                StartsWith(node);
            else if (methodName == "EndsWith")
                EndsWith(node);

            return  Expression.Default(typeof(bool));//base.VisitMethodCall(node);
        }

        protected override Expression VisitBinary(BinaryExpression node)
        {
            var operation = GetOperation(node);

            if (operation != null)
                IsNull(node, operation);
            return base.VisitBinary(node);
        }

        #endregion

        #region Visit Methods

        private void Any(MethodCallExpression node)
        {
            if (node.Method.Name == "Any" && _parameter != null)
            {
                Expression expArg = null;
                BinaryExpression expBody = null;

                Delegate lambda = null;

                if (node.Method.DeclaringType == typeof (Enumerable))
                {
                    Expression expTarget;
                    if ((expTarget = node.Arguments[1]).NodeType == ExpressionType.Lambda)
                    {
                        expArg = node.Arguments[0];
                        var expLambda = (LambdaExpression) expTarget;
                        expBody = (BinaryExpression) expLambda.Body;
                        var expParam = expLambda.Parameters[0];

                        var expValue = GetValueExpression(expBody.Left, expBody.Right);

                        if (expValue != null)
                        {
                            lambda = Expression.Lambda(expValue, new[] {expParam}).Compile();
                        }
                    }
                }

                if (expArg != null && lambda != null)
                {
                    var func = Expression.Lambda(expArg).Compile();
                    var items = func.DynamicInvoke() as IEnumerable;

                    if (items != null)
                    {
                        var operation = GetOperation(expBody);
                        var target = expBody.Right;
                        var joins = items.OfType<object>()
                            .Select(x => string.Format("({0} {1} {2})", _adaptor.Translate(lambda.DynamicInvoke(x)), operation, target))
                            .ToArray();

                        var find = node.ToString();
                        var replace = string.Join(" Or ", joins);

                        Literal.Replace(find, replace);
                    }
                }

            }
        }

        private void Contains(MethodCallExpression node)
        {
            if (node.Method.Name == "Contains" && node.Method.DeclaringType != null)
            {
                if (node.Method.DeclaringType == typeof (string))
                {
                    StringMethod(node, "{0} like '%{1}%'", "'{0}' like '%' + {1} + '%'");
                }
                else
                {
                    Expression expTarget = null;
                    Expression expValues = null;

                    if (node.Method.DeclaringType == typeof (Enumerable))
                    {
                        expTarget = node.Arguments[1];
                        expValues = node.Arguments[0];
                    }
                    else if (node.Method.DeclaringType.GetGenericTypeDefinition() == typeof (List<>))
                    {
                        expTarget = node.Arguments[0];
                        expValues = node.Object;
                    }

                    if (expValues != null)
                    {
                        var lambda = Expression.Lambda(expValues);
                        var values = lambda.Compile().DynamicInvoke() as IEnumerable;

                        if (values != null)
                        {
                            var joins = string.Join(", ", values.OfType<object>().Select(x => _adaptor.Translate(x)));
                            var find = node.ToString();
                            var replace = string.Format("{0} in ({1})", expTarget, joins);

                            Literal.Replace(find, replace);
                        }
                    }
                }
            }
        }

        private void Equals(MethodCallExpression node)
        {
            if (node.Method.Name == "Equals" && node.Method.DeclaringType != null)
            {
                var expTarget = node.Arguments[0];
                var expParam = node.Object;

                if (expParam != null)
                {
                    var lambda = Expression.Lambda(expParam).Compile();
                    var value = lambda.DynamicInvoke();

                    if (value != null)
                    {
                        value = _adaptor.Translate(value);
                        var find = node.ToString();
                        var replace = string.Format("{0} = {1}", expTarget, value);

                        Literal.Replace(find, replace);
                    }
                }
            }
        }

        private void StartsWith(MethodCallExpression node)
        {
            if (node.Method.Name == "StartsWith")
                StringMethod(node, "{0} like '{1}%'", "'{0}' like {1} + '%'");
        }

        private void EndsWith(MethodCallExpression node)
        {
            if (node.Method.Name == "EndsWith")
                StringMethod(node, "{0} like '%{1}'", "'{0}' like '%' + {1}");
        }

        private void IsNull(BinaryExpression node, string operation)
        {
            var expValue = GetValueExpression(node.Left, node.Right);

            if (expValue != null)
            {
                var value = Expression.Lambda(expValue).Compile().DynamicInvoke();
                var find = node.ToString();
                var replace = string.Empty;
                if (value == null)
                {
                    switch (operation)
                    {
                        case "=":
                            replace = string.Format("{0} Is Null", expValue == node.Left ? node.Right : node.Left);
                            break;
                        case "!=":
                            replace = string.Format("{0} Is Not Null", expValue == node.Left ? node.Right : node.Left);
                            break;
                    }
                }
                else
                {
                    value = _adaptor.Translate(value);
                    replace = string.Format("{0} {1} {2}", expValue == node.Left ? value : node.Left, operation, expValue == node.Left ? node.Right : value);
                }
                Literal.Replace(find, replace);
            }
        }

        #endregion

        #region Private Methods

        private static string GetParameterName(Expression exp)
        {
            if (exp.NodeType == ExpressionType.MemberAccess)
            {
                var expParam = ((MemberExpression) exp).Expression;
                if (expParam.NodeType == ExpressionType.Parameter)
                    return ((ParameterExpression) expParam).Name;
            }
            return null;
        }

        private Expression GetValueExpression(Expression expLeft, Expression expRight)
        {
            var leftName = GetParameterName(expLeft);
            var rightName = GetParameterName(expRight);

            if (leftName == _parameter.Name)
            {
                return expRight;
            }
            if (rightName == _parameter.Name)
            {
                return expLeft;
            }
            return null;
        }

        private string GetOperation(BinaryExpression node)
        {
            switch (node.NodeType)
            {
                case ExpressionType.Equal:
                    return "=";
                case ExpressionType.NotEqual:
                    return "!=";
                case ExpressionType.GreaterThan:
                    return ">";
                case ExpressionType.GreaterThanOrEqual:
                    return ">=";
                case ExpressionType.LessThan:
                    return "<";
                case ExpressionType.LessThanOrEqual:
                    return "<=";
                default:
                    return null;
            }
        }

        private void StringMethod(MethodCallExpression methodCall, string memberLeft, string memberRight)
        {
            if (methodCall.Method.DeclaringType == typeof(string))
            {
                var expLeft = methodCall.Object;
                var expRight = methodCall.Arguments[0];

                var expValue = GetValueExpression(expLeft, expRight);

                if (expValue != null)
                {
                    var value = Expression.Lambda(expValue).Compile().DynamicInvoke() as string;
                    if (value != null)
                    {
                        var find = methodCall.ToString();
                        var replace = expValue == expRight
                            ? string.Format(memberLeft, expLeft, value)
                            : string.Format(memberRight, value, expRight);

                        Literal.Replace(find, replace);
                    }
                }
            }
        }

        #endregion
    }
}
